#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (c) 2011
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Author: Jesus Carrero <j.o.carrero@gmail.com>
# Mountain View, CA

""" calibrate volatility curve using splines. """

from scipy import c_, zeros, arange, linspace
from numpy import asarray
from utils.FemBases import LagrangeP1, LagrangeP2

from models.CalibBase import CalibBase
from models.NumericalOptionGBM import NumericalOptionGBM
from solvers.EuropeanTimeStepper import EuropeanTimeStepper

import logging


class CalibVolCurve(CalibBase):
    """ calibrate volatility using splines.
        Adaptive stepper using a PID controller, based on::

        @article{PIDpaper,
            author =  {A. M. P. Valli and G. F. Carey and A. L. G. A. Coutinho},
            title =   {Control strategies for timestep selection in finite element
                       simulation of incompressible flows and coupled
                       reaction-convection-diffusion processes},
            journal = {Int. J. Numer. Meth. Fluids},
                       volume =  47,
                       year =    2005,
                       pages =   {201-231},
        }
    """

    __slots__ = ['m_log_progress', 'm_lagrange1', 'm_lagrange2',
                 'm_market_val']

    def __init__(self):
        """ instantiate an object of type CalibVolCurve. """
        CalibBase.__init__(self)

        (self.m_tmin, self.m_tmax, self.m_min_price,
         self.m_max_price) = (None, None, None, None)

        (self.m_indx, self.m_time_spacing, self.m_nt_intervals) = \
            (None, None, None)

        self.m_lagrange1 = LagrangeP1()
        self.m_lagrange2 = LagrangeP2()

        self.m_market_val = None
        self.m_ntime_steps = None

        self.log_progress = logging.getLogger('CalibVolCurve')

    def get_price_range(self):
        """ get prince range. """

        return (self.m_min_price, self.m_max_price)

    def set_grid_spacing(self, nt_intervals, ntime_steps):
        """ grid as used for calibration engine. """

        (self.m_nt_intervals, self.m_ntime_steps) = (nt_intervals,
                ntime_steps)

    def set_market_value(self, opt_prices):
        """ observe option prices """

        self.m_market_val = opt_prices

    def __price_single_option(self, desp, sigma):
        """
            Strike, Price, Time, Style[A, E], Contract[P, C]
            Unfortunally you have to assemble the matrix from scratch (:
            Parallel computing is the solution
        """

        (strike, price, interest_rate, time2_exp, style, cont_type) = desp

        price_engine = NumericalOptionGBM(style, time2_exp, price, strike,
                            self.m_ntime_steps, self.m_nt_intervals,
                            self.m_min_price, self.m_max_price)
        price_engine.set_option_cos(float(sigma), 0, interest_rate, True,
                True, True)
        price_engine.set_price_mode('price')
        price_engine.set_shape_functions(self.m_lagrange2)

        time_stepper = EuropeanTimeStepper()
        time_stepper.set_dolog(False)
        time_stepper.set_number_steps(self.m_ntime_steps)
        time_stepper.set_time2simulate(time2_exp)
        price_engine.set_time_stepper(time_stepper)

        if cont_type == 'C':
            return price_engine.price_call()
        else:
            return price_engine.price_put()

    def __price_options(self, sig):
        """
            This is where the power of computation is needed.
            I structured the code in order to ditribute this function.
            MPI, cuda, threding, whatever you want.
        """

        contract_id = self.get_cal_data()

        time2_mat = self.get_times2_maturity()
        time_spacing = self.m_time_spacing
        time_interval = c_[time_spacing[:-1], time_spacing[1:]]
        model_value = zeros(self.m_market_val.shape)

        for t_interval in time_interval:
            cont_posi = self.m_indx[(t_interval[0] <= time2_mat)
                                    * (time2_mat < t_interval[1])]
            if len(cont_posi):
                for posi in cont_posi:
                    model_value[posi] = \
                        self.__price_single_option(contract_id[posi],
                            sig)
        return model_value

    def _cost_function(self, sig):
        """ user define cost function. """

        if sig < 0:
            return -10000 * sig

        model_value = self.__price_options(sig)
        return sum((self.m_market_val - model_value) ** 2)

    def calibrate(self):
        """ calibrate model. """

        time2_mat = self.get_times2_maturity()
        (tmin, tmax) = (min(time2_mat) * 0.95, max(time2_mat) * 1.1)
        (self.m_min_price, self.m_max_price) = (10, 130)

        self.m_time_spacing = asarray(linspace(tmin, tmax, 10, 'False'))
        self.m_indx = arange(0, self.m_time_spacing.size + 1)
        return (self.m_time_spacing, self._calibrate())


